CSS Font-Size Keyword Converter - Developer Documentation

Version: 0.7.0

Last Updated: 2025

Maintainer: [Your Name]


Table of Contents

  1. Code Organization
  2. Common Modification Tasks
  3. Debugging Guide
  4. Known Issues & Limitations
  5. Design Decisions & Rationale
  6. Testing Strategy
  7. Release Process
  8. Code Conventions
  9. Dependencies & Compatibility

Code Organization

File Structure

The plugin is a single Python file (plugin.py) organized as follows:

Top Section (Lines 1-30):

Helper Classes (Lines 31-70):

Utility Functions (Lines 71-180):

Main Entry Point (Lines 181-400):

UI Functions (Lines 401-end):

Function Dependencies

run(bk) ├── build_patterns(config) [nested] ├── strip_css_comments() └── show_file_selector() ├── show_config_window() │ ├── ensure_config_dir() │ ├── get_saved_configs() │ └── validate_backup_folder() ├── show_preview_window() │ ├── strip_css_comments() │ └── build_patterns() ├── show_unsafe_css_dialog() ├── show_shorthand_preview_dialog() └── [conversion logic] ├── strip_css_comments() ├── restore_css_comments() └── is_unsafe_shorthand()

Key Data Flow

  1. Configuration → Stored in Sigil preferences + JSON files
  2. Scanning → Files with keywords → File list with metadata
  3. User Selection → Checked files → Selected file list
  4. Categorization → Changes → safe_changes, shorthand_changes, unsafe_items
  5. Conversion → Modified text → Write to book
  6. Undo → Memory backup → Restore original content

Common Modification Tasks

Adding a New Keyword

Step 1: Add to keywords list in build_patterns() (around line 220):

keywords = [
    # ... existing keywords
    ('new-keyword', 'newkeyword', 'newkeyword_unit'),
]

Step 2: Add default values in run() function (around line 195):

config = {
    # ... existing configs
    'newkeyword': prefs.get('newkeyword', '1.5'),
    'newkeyword_unit': prefs.get('newkeyword_unit', 'em'),
}

Step 3: Add to UI in show_config_window() (around line 550):

keywords = [
    # ... existing keywords
    ('new-keyword', 'newkeyword'),
]

Step 4: Add to defaults in reset_defaults() (around line 730):

defaults = {
    # ... existing defaults
    'newkeyword': '1.5',
}

Adding a New Unit Type

To add support for ch, vw, or other CSS units:

Step 1: Modify radio button creation in show_config_window() (around line 580):

# Add new radio button column
ch_header = Label(config_frame, text="ch", font=('Arial', 10, 'bold'), width=5)
ch_header.grid(row=0, column=4, pady=5)

# In the loop for each keyword:
Radiobutton(config_frame, text="", variable=unit_var,
           value='ch').grid(row=idx, column=4, pady=3)

Step 2: Update bulk set buttons:

def set_all_ch():
    for unit_var in unit_vars.values():
        unit_var.set('ch')

all_ch_btn = Button(bulk_frame, text="All ch", command=set_all_ch, width=10)
all_ch_btn.pack(side=LEFT, padx=5)

Adding New Validation Rules

To add validation to is_unsafe_shorthand() (around line 155):

def is_unsafe_shorthand(css_line):
    # ... existing checks
    
    # New rule: reject if contains attr() function
    if re.search(r'\battr\s*\(', font_value, re.IGNORECASE):
        return True
    
    return False

Modifying UI Layout

Changing window size: Look for .geometry() calls:

# In show_config_window():
window_width = min(750, int(screen_width * 0.8))  # Adjust multiplier

Changing colors: Search for color codes:

# Find and replace hex codes like:
background='#ffd6d6'  # Pink
background='#cce5ff'  # Blue
foreground='#FF0000'  # Red

Adding new checkbox to config: Add after existing checkboxes (around line 615):

new_feature_var = BooleanVar(value=config.get('new_feature', 'false') == 'true')
new_check = Checkbutton(frame, text="Enable new feature", variable=new_feature_var)
new_check.pack(anchor=W, pady=(0, 5))

Adding File Filters

To add a filter for SVG files in the file selector (around line 1180):

Step 1: Add checkbox variable:

show_svg = BooleanVar(value=False)

Step 2: Add checkbox to filter_frame:

cb_svg = Checkbutton(filter_frame, text="Show SVG Files", variable=show_svg,
    command=lambda: [show_all.set(False), show_text.set(False), 
                     show_css.set(False), update_filter()])
cb_svg.pack(side=LEFT, padx=5)

Step 3: Modify populate_checkboxes():

if show_svg.get():
    filtered_files = [f for f in files_with_keywords if f['type'] == 'svg']

Debugging Guide

Testing Without Sigil

Create a mock book container object:

class MockBook:
    def __init__(self):
        self.prefs = {}
        self.files = {
            'css1': 'body { font-size: small; }',
            'html1': '<p style="font-size: large;">Test</p>'
        }
    
    def getPrefs(self):
        return self.prefs
    
    def savePrefs(self, prefs):
        self.prefs = prefs
    
    def css_iter(self):
        return [('css1', 'style.css')]
    
    def text_iter(self):
        return [('html1', 'page.html')]
    
    def readfile(self, file_id):
        return self.files.get(file_id, '')
    
    def writefile(self, file_id, content):
        self.files[file_id] = content

# Test:
bk = MockBook()
run(bk)

Common Failure Points

1. Regex Pattern Errors

# Debug pattern matching:
pattern = compiled_patterns[0][0]  # First pattern
test_line = "font-size: small;"
print(f"Pattern: {pattern.pattern}")
print(f"Match: {pattern.search(test_line)}")

2. Comment Placeholder Collision

# Test placeholder uniqueness:
css = "/* test */ body { font-size: small; }"
text, pmap = strip_css_comments(css)
print(f"Placeholders: {list(pmap.keys())}")
restored = restore_css_comments(text, pmap)
assert restored == css

3. Checkbox State Issues

# Debug checkbox state:
for widget in checkbox_frame.winfo_children():
    print(f"{widget} state: {widget['state']}")

4. Backup Validation Failures

# Test backup validation:
is_valid, error = validate_backup_folder('/path/to/folder')
print(f"Valid: {is_valid}, Error: {error}")

Verbose Logging

Add debug prints throughout:

def strip_css_comments(css_text):
    print(f"DEBUG: Input length: {len(css_text)}")
    # ... processing
    print(f"DEBUG: Found {len(placeholder_map)} comments")
    return text_no_comments, placeholder_map

To disable in production, use a global flag:

DEBUG = False  # Set at top of file

def debug_print(msg):
    if DEBUG:
        print(msg)
        sys.stdout.flush()

Testing Regex in Isolation

import re

keyword = 'small'
escaped = re.escape(keyword)
pattern = rf'(font-size\s*:\s*){escaped}(\s*(?=[;}}"\'\!]|$))'
regex = re.compile(pattern, re.IGNORECASE | re.MULTILINE)

test_cases = [
    'font-size: small;',
    'font-size:small;',
    'font-size: SMALL;',
    'font-size: small !important;',
    'font-size: smaller;',  # Should not match
]

for test in test_cases:
    match = regex.search(test)
    print(f"{test:30} -> {bool(match)}")

Platform-Specific Issues

Windows:

macOS:

Linux:


Known Issues & Limitations

Edge Cases Not Handled

1. Font Shorthand with System Keywords

font: small caption;  /* 'caption' is system font, not family */

Plugin will convert 'small' but result may be incorrect.

2. Multiple Font-Size in Same Declaration

font-size: large; font-size: small;  /* Last one wins */

Plugin converts both, even though only last matters.

3. Keywords in Attribute Selectors

[title~="small"] { font-size: 12px; }

Pattern won't match (correct behavior), but worth noting.

4. CSS Variables Containing Keywords

--my-size: small;  /* Variable assignment */
font-size: var(--my-size);  /* Usage */

Plugin won't convert variable assignment or usage (correct for safety).

5. Very Long Lines

If a CSS line exceeds ~10,000 characters, regex performance may degrade significantly.

Performance Characteristics

Scaling:

Memory usage:

Regex Performance:

Tkinter Quirks

Canvas Scrolling:

Dialog Modality:

Text Widget Performance:

Sigil Version Compatibility

Tested with:

API assumptions:

Breaking changes to watch for:


Design Decisions & Rationale

Why UUID Placeholders Instead of Sequential Numbers?

Decision: Use uuid.uuid4().hex for comment placeholders

Rationale:

Rejected alternatives:

Why Memory Backup + Optional External Backup?

Decision: Always store originals in memory, optionally write to disk

Rationale:

Rejected alternatives:

Why Two Dialogs for Unsafe/Safe Shorthand?

Decision: Separate "Unsafe CSS Detected" and "Review Safe Shorthand Changes"

Rationale:

Rejected alternatives:

Why Checkboxes Instead of Listbox?

Decision: Individual checkboxes for each file

Rationale:

Rejected alternatives:

Tradeoffs Made

Simplicity vs Features:

Performance vs Safety:

Flexibility vs Usability:


Testing Strategy

Manual Test Cases

Create two test files (provided separately):

Pre-Conversion Checks:

  1. Plugin loads without errors
  2. Finds both test files
  3. Counts matches accurately
  4. Configuration loads with defaults

Conversion Tests:

Basic (Shorthand Disabled):

  1. Select both files, click Preview
  2. Verify only font-size: changes shown
  3. Apply changes
  4. Verify conversions in files
  5. Click Undo
  6. Verify files restored to original

Shorthand (Enabled):

  1. Open Configuration, enable shorthand
  2. Select both files, click Apply
  3. Should see "Unsafe CSS Detected" dialog
  4. Click Continue
  5. Should see "Review Safe Shorthand Changes" dialog
  6. Click "Apply All"
  7. Verify both font-size: and safe font: converted
  8. Verify unsafe items skipped

Edge Cases:

  1. Cancel at each dialog, verify no changes made
  2. Filter by HTML only, verify CSS file hidden
  3. Change units to rem, verify rem used in output
  4. Create external backup, verify files created with timestamp
  5. Modify numeric values, verify custom values used

Error Handling:

  1. Select invalid backup folder, verify error shown
  2. Enter negative number in config, verify validation error
  3. Enter non-numeric value, verify validation error

Regression Testing

After any code change, verify:

  1. All keyword conversions still work
  2. Comments preserved correctly
  3. Shorthand safety detection works
  4. Backup/undo cycle works
  5. Configuration save/load works
  6. No crashes in any workflow

User Acceptance Criteria

Plugin is ready for release when:


Release Process

Version Numbering

Format: MAJOR.MINOR.PATCH

Increment:

Current: 0.7.0

Pre-Release Checklist

Packaging for Sigil

File structure:

NewUnit/ ├── plugin.py # Main plugin file ├── plugin.xml # Sigil plugin manifest └── README.md # User instructions

plugin.xml example:

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
  <name>CSS Font-Size Keyword Converter</name>
  <type>edit</type>
  <author>Your Name</author>
  <description>Converts CSS font-size keywords to em/rem units</description>
  <version>0.7.0</version>
  <engine>python3.4</engine>
</plugin>

Creating plugin ZIP:

  1. Place files in NewUnit/ folder
  2. Create ZIP: NewUnit_v0.7.0.zip
  3. Test installation in clean Sigil instance

Distribution

Official Sigil Plugin Repository:
Submit to: https://www.mobileread.com/forums/forumdisplay.php?f=237

GitHub Release:

  1. Create tag: v0.7.0
  2. Upload ZIP file
  3. Write release notes

Release Notes Template:

## Version 0.7.0 - YYYY-MM-DD

### New Features
- Feature description

### Bug Fixes
- Fix description

### Known Issues
- Issue description

### Upgrade Notes
- Migration instructions if needed

Changelog Management

Keep CHANGELOG.md updated with:


Code Conventions

Naming Patterns

Functions:

Variables:

Constants:

Exceptions:

Global vs Local Scope

Use Global Scope For:

Use Local Scope For:

Avoid:

Error Handling Patterns

File Operations:

try:
    # operation
except (IOError, OSError) as e:
    print(f'Error: {e}')
    sys.stdout.flush()
    # continue or return

User Input Validation:

try:
    value = float(input_str)
    if value <= 0:
        show_error("Must be positive")
        return False
except ValueError:
    show_error("Must be a number")
    return False

Always:

Comment Style

Module/Function Docstrings:

def function_name(param):
    """
    Brief description of what function does
    
    Args:
        param: Description
    
    Returns:
        Description of return value
    """

Inline Comments:

Section Headers:

# ===== SECTION NAME =====
# For major sections of code

Dependencies & Compatibility

Python Version Requirements

Minimum: Python 3.4 (matches Sigil's requirement)
Recommended: Python 3.8+
Tested: Python 3.9, 3.10, 3.11

Features used that require 3.4+:

To support Python 3.4-3.5:
Replace f-strings with .format():

# Instead of:
f"Error: {msg}"

# Use:
"Error: {}".format(msg)

Standard Library Dependencies

All imports are from standard library:

No third-party dependencies - Plugin is self-contained.

Tkinter Version Differences

Python 3.x uses tkinter (lowercase)

from tkinter import *
from tkinter import ttk, filedialog, messagebox

Not compatible with Python 2.x (which used Tkinter)

Tkinter features used:

Sigil API Requirements

Book Container Methods Used:

bk.getPrefs() # Get plugin preferences dict
bk.savePrefs(dict) # Save plugin preferences
bk.css_iter() # Iterate CSS files: (id, href)
bk.text_iter() # Iterate text files: (id, href)
bk.readfile(id) # Read file content (bytes or str)
bk.writefile(id, str) # Write file content

Assumptions:

Version Notes:

Cross-Platform Considerations

File Paths:

Tkinter Rendering:

Configuration Storage:

Line Endings:

Permissions:


Appendix: Quick Reference

Common File Locations

Plugin file: plugin.py
Config storage: ~/.sigil/NewUnit/configs/*.json
Temp backup test: <backup_folder>/.write_test_<uuid>.tmp

Key Functions Quick Reference

Function Purpose Returns
run(bk) Main entry point 0 on success
build_patterns(cfg) Create regex patterns List of (pattern, replacer, type)
strip_css_comments(text) Remove comments (text, placeholder_map)
restore_css_comments(text, map) Restore comments text
is_unsafe_shorthand(line) Check safety bool
validate_backup_folder(path) Check writability (bool, error_msg)

Configuration Keys

Key Type Default Description
xxsmall string '0.6' xx-small value
xxsmall_unit string 'em' xx-small unit
(repeat for all 9 keywords)
convert_font_shorthand string 'false' Enable shorthand
create_backup string 'false' Enable external backup
backup_path string '' Backup folder path

UI Color Codes

Element Color Hex
Regular "before" Pink #ffd6d6
Shorthand "before" Blue #cce5ff
[SHORTHAND] label Red #FF0000
"after" Green #d6ffd6
File headers Blue #2196F3
Warnings Orange #FF6600

Exit Codes

Code Meaning
0 Success
-1 Not run from Sigil

Document Version: 1.0
Last Updated: 2025-01-15
Contact: [maintainer email/github]